﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;

// Original code comments:
// PathGeometryHelper.cs by Charles Petzold, December 2007
//
// This source code obtained from MSDN Magasine Code Foundation (March 2008) and 
// the URL is http://msdn.microsoft.com/en-us/magazine/cc337985.aspx?code=true&level=root,Petzold.Shapes&file=PathGeometryHelper.cs.

namespace Xceed.Silverlight.Utils
{
  internal class PathGeometryHelper
  {
    public PathGeometry FlattenGeometry( Geometry geometry, double tolerance )
    {
      if( geometry == null )
        return new PathGeometry();

      PathGeometry destinationGeometry = new PathGeometry();
      this.FlattenGeometry( destinationGeometry, geometry, tolerance, Matrix.Identity );

      return destinationGeometry;
    }

    private void FlattenGeometry(
      PathGeometry destinationPathGeometry,
      Geometry sourceGeometry,
      double tolerance,
      Matrix previousSourceGeometryTransformMatrix )
    {
      Matrix sourceGeometryTransformMatrix = TransformHelper.GetTransformValue( sourceGeometry.Transform );

      MatrixHelper matxHelper = new MatrixHelper( sourceGeometryTransformMatrix ) * new MatrixHelper( previousSourceGeometryTransformMatrix );

      Matrix matrix = matxHelper.Value;
      PathFigure figure = null;

      if( sourceGeometry is GeometryGroup )
      {
        GeometryGroup group = sourceGeometry as GeometryGroup;

        foreach( Geometry child in group.Children )
        {
          this.FlattenGeometry( destinationPathGeometry, child, tolerance, matrix );
        }

        return;
      }

      if( sourceGeometry is LineGeometry )
      {
        figure = this.CreateFigureForLineGeometry( sourceGeometry as LineGeometry, matrix );
      }
      else if( sourceGeometry is RectangleGeometry )
      {
        figure = this.CreateFigureForRectangleGeometry( sourceGeometry as RectangleGeometry, matrix, tolerance );
      }
      else if( sourceGeometry is EllipseGeometry )
      {
        figure = this.CreateFigureForEllipseGeometry( sourceGeometry as EllipseGeometry, matrix, tolerance );
      }
      else if( sourceGeometry is PathGeometry )
      {
        PathGeometry pathGeometry = sourceGeometry as PathGeometry;
        destinationPathGeometry.FillRule = pathGeometry.FillRule;
        figure = this.CreateFigureForPathGeometry( pathGeometry, matrix, tolerance );
      }

      destinationPathGeometry.Figures.Add( figure );
    }

    private PathFigure CreateFigureForPathGeometry(
      PathGeometry pathGeometry,
      Matrix matrix,
      double tolerance )
    {
      if( pathGeometry == null )
        throw new ArgumentNullException( "pathGeometry" );

      PathFigure destinationFigure = new PathFigure();
      int figureCount = pathGeometry.Figures.Count;

      for( int i = 0; i < figureCount; i++ )
      {
        PathFigure sourceFigure = pathGeometry.Figures[ i ];

        destinationFigure.IsFilled = sourceFigure.IsFilled;
        destinationFigure.IsClosed = sourceFigure.IsClosed;
        destinationFigure.StartPoint = matrix.Transform( sourceFigure.StartPoint );
        Point lastPoint = destinationFigure.StartPoint;

        int segmentCount = sourceFigure.Segments.Count;

        for( int y = 0; y < segmentCount; y++ )
        {
          PathSegment sourceSegment = sourceFigure.Segments[ y ];

          PolyLineSegment destinationSegment = new PolyLineSegment();

          if( sourceSegment is LineSegment )
          {
            LineSegment lineSegment = sourceSegment as LineSegment;
            lastPoint = matrix.Transform( lineSegment.Point );
            destinationSegment.Points.Add( lastPoint );
          }
          else if( sourceSegment is PolyLineSegment )
          {
            PolyLineSegment polyLineSegment = sourceSegment as PolyLineSegment;

            foreach( Point point in polyLineSegment.Points )
            {
              lastPoint = matrix.Transform( point );
              destinationSegment.Points.Add( lastPoint );
            }
          }
          else if( sourceSegment is BezierSegment )
          {
            BezierSegment bezSeg = sourceSegment as BezierSegment;

            Point pt0 = lastPoint;
            Point pt1 = matrix.Transform( bezSeg.Point1 );
            Point pt2 = matrix.Transform( bezSeg.Point2 );
            Point pt3 = matrix.Transform( bezSeg.Point3 );

            this.FlattenCubicBezier( destinationSegment.Points, pt0, pt1, pt2, pt3, tolerance );

            lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
          }
          else if( sourceSegment is PolyBezierSegment )
          {
            PolyBezierSegment polyBezSeg = sourceSegment as PolyBezierSegment;

            PointCollection polyBezierSegmentPoints = polyBezSeg.Points;
            int pointCount = polyBezierSegmentPoints.Count;

            for( int bez = 0; bez < pointCount; bez += 3 )
            {
              if( bez + 2 > pointCount - 1 )
                break;

              Point pt0 = lastPoint;
              Point pt1 = matrix.Transform( polyBezierSegmentPoints[ bez ] );
              Point pt2 = matrix.Transform( polyBezierSegmentPoints[ bez + 1 ] );
              Point pt3 = matrix.Transform( polyBezierSegmentPoints[ bez + 2 ] );

              this.FlattenCubicBezier( destinationSegment.Points, pt0, pt1, pt2, pt3, tolerance );

              lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
            }
          }
          else if( sourceSegment is QuadraticBezierSegment )
          {
            QuadraticBezierSegment quadBezSeg = sourceSegment as QuadraticBezierSegment;
            Point pt0 = lastPoint;
            Point pt1 = matrix.Transform( quadBezSeg.Point1 );
            Point pt2 = matrix.Transform( quadBezSeg.Point2 );

            this.FlattenQuadraticBezier( destinationSegment.Points, pt0, pt1, pt2, tolerance );

            lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
          }
          else if( sourceSegment is PolyQuadraticBezierSegment )
          {
            PolyQuadraticBezierSegment polyQuadBezSeg = sourceSegment as PolyQuadraticBezierSegment;

            PointCollection points = polyQuadBezSeg.Points;
            int pointCount = points.Count;

            for( int bez = 0; bez < pointCount; bez += 2 )
            {
              if( bez + 1 > pointCount - 1 )
                break;

              Point pt0 = lastPoint;
              Point pt1 = matrix.Transform( points[ bez ] );
              Point pt2 = matrix.Transform( points[ bez + 1 ] );

              this.FlattenQuadraticBezier( destinationSegment.Points, pt0, pt1, pt2, tolerance );

              lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
            }
          }
          else if( sourceSegment is ArcSegment )
          {
            ArcSegment arcSeg = sourceSegment as ArcSegment;

            PointCollection destinationPoints = destinationSegment.Points;

            this.FlattenArc( destinationPoints,
              lastPoint,
              matrix.Transform( arcSeg.Point ),
              matrix.Transform( new Point( arcSeg.Size.Width, 0 ) ).X,
              matrix.Transform( new Point( 0, arcSeg.Size.Height ) ).Y,
              arcSeg.RotationAngle,
              arcSeg.IsLargeArc,
              arcSeg.SweepDirection == SweepDirection.Counterclockwise,
              tolerance );

            if( destinationPoints.Count > 0 )
              lastPoint = destinationPoints[ destinationPoints.Count - 1 ];
          }

          destinationFigure.Segments.Add( destinationSegment );
        }
      }

      return destinationFigure;
    }

    private PathFigure CreateFigureForEllipseGeometry(
      EllipseGeometry ellipseGeometry,
      Matrix matrix,
      double tolerance )
    {
      if( ellipseGeometry == null )
        throw new ArgumentNullException( "ellipseGeometry" );

      PathFigure figure = new PathFigure();
      PolyLineSegment segment = new PolyLineSegment();

      int max = ( int )( 4 * ( ellipseGeometry.RadiusX + ellipseGeometry.RadiusY ) / tolerance );

      for( int i = 0; i < max; i++ )
      {
        double x = ellipseGeometry.Center.X + ellipseGeometry.RadiusX * Math.Sin( i * 2 * Math.PI / max );
        double y = ellipseGeometry.Center.Y - ellipseGeometry.RadiusY * Math.Cos( i * 2 * Math.PI / max );
        Point pt = matrix.Transform( new Point( x, y ) );

        if( i == 0 )
        {
          figure.StartPoint = pt;
        }
        else
        {
          segment.Points.Add( pt );
        }
      }

      figure.IsClosed = true;
      figure.Segments.Add( segment );
      return figure;
    }

    private PathFigure CreateFigureForRectangleGeometry(
      RectangleGeometry rectangleGeometry,
      Matrix matrix,
      double tolerance )
    {
      if( rectangleGeometry == null )
        throw new ArgumentNullException( "rectangleGeometry" );

      PathFigure figure = new PathFigure();
      PolyLineSegment segment = new PolyLineSegment();

      if( rectangleGeometry.RadiusX == 0 || rectangleGeometry.RadiusY == 0 )
      {
        figure.StartPoint = matrix.Transform( new RectHelper( rectangleGeometry.Rect ).TopLeft );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).TopRight ) );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).BottomRight ) );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).BottomLeft ) );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).TopLeft ) );
      }
      else
      {
        double radiusX = Math.Min( rectangleGeometry.Rect.Width / 2, rectangleGeometry.RadiusX );
        double radiusY = Math.Min( rectangleGeometry.Rect.Height / 2, rectangleGeometry.RadiusY );
        Rect rect = rectangleGeometry.Rect;

        figure.StartPoint = matrix.Transform( new Point( rect.Left + radiusX, rect.Top ) );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Right - radiusX, rect.Top ),
          new Point( rect.Right, rect.Top + radiusY ),
          radiusX, radiusY, tolerance );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Right, rect.Bottom - radiusY ),
          new Point( rect.Right - radiusX, rect.Bottom ),
          radiusX, radiusY, tolerance );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Left + radiusX, rect.Bottom ),
          new Point( rect.Left, rect.Bottom - radiusY ),
          radiusX, radiusY, tolerance );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Left, rect.Top + radiusY ),
          new Point( rect.Left + radiusX, rect.Top ),
          radiusX, radiusY, tolerance );
      }

      figure.IsClosed = true;
      figure.Segments.Add( segment );
      return figure;
    }

    private PathFigure CreateFigureForLineGeometry(
      LineGeometry lineGeometry,
      Matrix matrix )
    {
      if( lineGeometry == null )
        throw new ArgumentNullException( "lineGeometry" );

      PathFigure figure = new PathFigure();
      PolyLineSegment segment = new PolyLineSegment();

      figure.StartPoint = matrix.Transform( lineGeometry.StartPoint );
      segment.Points.Add( matrix.Transform( lineGeometry.EndPoint ) );

      figure.Segments.Add( segment );
      return figure;
    }

    private void FlattenCubicBezier(
      PointCollection points,
      Point ptStart,
      Point ptCtrl1,
      Point ptCtrl2,
      Point ptEnd,
      double tolerance )
    {
      int max = ( int )( ( ( new PointHelper( ptCtrl1 ) - new PointHelper( ptStart ) ).Length +
                           ( new PointHelper( ptCtrl2 ) - new PointHelper( ptCtrl1 ) ).Length +
                           ( new PointHelper( ptEnd ) - new PointHelper( ptCtrl2 ) ).Length ) / tolerance );

      for( int i = 0; i <= max; i++ )
      {
        double t = ( double )i / max;

        double x = ( 1 - t ) * ( 1 - t ) * ( 1 - t ) * ptStart.X +
                   3 * t * ( 1 - t ) * ( 1 - t ) * ptCtrl1.X +
                   3 * t * t * ( 1 - t ) * ptCtrl2.X +
                   t * t * t * ptEnd.X;

        double y = ( 1 - t ) * ( 1 - t ) * ( 1 - t ) * ptStart.Y +
                   3 * t * ( 1 - t ) * ( 1 - t ) * ptCtrl1.Y +
                   3 * t * t * ( 1 - t ) * ptCtrl2.Y +
                   t * t * t * ptEnd.Y;

        points.Add( new Point( x, y ) );
      }
    }

    private void FlattenQuadraticBezier(
      PointCollection points,
      Point ptStart,
      Point ptCtrl,
      Point ptEnd,
      double tolerance )
    {
      int max = ( int )( ( ( new PointHelper( ptCtrl ) - new PointHelper( ptStart ) ).Length +
                          ( new PointHelper( ptEnd ) - new PointHelper( ptCtrl ) ).Length ) / tolerance );

      for( int i = 0; i <= max; i++ )
      {
        double t = ( double )i / max;

        double x = ( 1 - t ) * ( 1 - t ) * ptStart.X +
                   2 * t * ( 1 - t ) * ptCtrl.X +
                   t * t * ptEnd.X;

        double y = ( 1 - t ) * ( 1 - t ) * ptStart.Y +
                   2 * t * ( 1 - t ) * ptCtrl.Y +
                   t * t * ptEnd.Y;

        points.Add( new Point( x, y ) );
      }
    }

    private void FlattenArc(
      IList<Point> points,
      Point pt1,
      Point pt2,
      double radiusX,
      double radiusY,
      double angleRotation,
      bool isLargeArc,
      bool isCounterclockwise,
      double tolerance )
    {
      // Get info about chord that connects both points
      Point midPoint = new Point( ( pt1.X + pt2.X ) / 2, ( pt1.Y + pt2.Y ) / 2 );
      Vector vect = new PointHelper( pt2 ) - new PointHelper( pt1 );
      double halfChord = vect.Length / 2;

      // Get vector from chord to center
      Vector vectRotated;

      // (comparing two Booleans here!)
      if( isLargeArc == isCounterclockwise )
      {
        vectRotated = new Vector( -vect.Y, vect.X );
      }
      else
      {
        vectRotated = new Vector( vect.Y, -vect.X );
      }

      vectRotated.Normalize();

      // Distance from chord to center 
      double centerDistance = Math.Sqrt( radiusY * radiusY - halfChord * halfChord );

      // Calculate two center points
      Point center = midPoint + centerDistance * vectRotated;

      // Get angles from center to the two points
      double angle1 = Math.Atan2( pt1.Y - center.Y, pt1.X - center.X );
      double angle2 = Math.Atan2( pt2.Y - center.Y, pt2.X - center.X );

      // (another comparison of two Booleans!)
      if( isLargeArc == ( Math.Abs( angle2 - angle1 ) < Math.PI ) )
      {
        if( angle1 < angle2 )
        {
          angle1 += 2 * Math.PI;
        }
        else
        {
          angle2 += 2 * Math.PI;
        }
      }

      // Calculate number of points for polyline approximation
      int max = ( int )( ( 4 * ( radiusX + radiusY ) * Math.Abs( angle2 - angle1 ) / ( 2 * Math.PI ) ) / tolerance );

      // Loop through the points
      for( int i = 0; i <= max; i++ )
      {
        double angle = ( ( max - i ) * angle1 + i * angle2 ) / max;
        double x = center.X + radiusY * Math.Cos( angle );
        double y = center.Y + radiusY * Math.Sin( angle );

        // Transform the point back
        points.Add( new Point( x, y ) );
      }
    }

    // Helper routine for RectangleGeometry
    private void AddCorner(
      PolyLineSegment segDst,
      Matrix matx,
      Point pt1,
      Point pt2,
      double radiusX,
      double radiusY,
      double tolerance )
    {
      List<Point> points = new List<Point>();

      this.FlattenArc( points,
        pt1,
        pt2,
        radiusX,
        radiusY,
        0,
        false,
        false,
        tolerance );

      int pointsCount = points.Count;

      for( int i = 0; i < pointsCount; i++ )
        segDst.Points.Add( matx.Transform( points[ i ] ) );
    }
  }
}
